package cn.song.util.easyexcel;

import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.write.merge.AbstractMergeStrategy;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;

import java.io.File;
import java.io.OutputStream;
import java.util.*;

/**
 * 增强版通用Excel导出工具类(用于合并单元格导出)
 * <p>
 *  使用场景示例：
 *  <pre>
 *      场景1：主从表结构（退货包裹和货品）
 *      createIdBasedMergeStrategy(
 *           dto -> dto.getPackageId(),    // 按包裹ID合并
 *           dto -> dto.getItems().size(), // 每个包裹占多行（根据货品数）
 *           0, 1, 2, 3, 4, 5, 6, 7, 8, 9  // 合并包裹信息列
 *     )
 *     场景2：分组数据（部门员工列表）
 *     createIdBasedMergeStrategy(
 *           emp -> emp.getDeptId(),       // 按部门ID合并
 *           emp -> 1,                     // 每个员工占一行
 *           0, 1                          // 合并部门ID和部门名称列
 *     )
 *     场景3：时间序列数据（按日期合并）
 *     createIdBasedMergeStrategy(
 *           data -> data.getDate(),       // 按日期合并
 *           data -> 1,                    // 每条记录占一行
 *           0                             // 合并日期列
 *     )
 *     场景4：微服务间文件传输
 *     // 1. 查询数据
 *     // 2. 转换为导出DTO
 *     // 3. 配置合并策略
 *     // 4. 创建导出配置
 *     // 5. 创建内存输出流
 *     ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
 *     // 6. 执行导出
 *     EnhancedExcelExporter.export(outputStream, "退货管理", exportData, config);
 *     // 7. 获取字节数组
 *     byte[] excelBytes = outputStream.toByteArray();
 *     // 8. 发送到其他服务
 *     otherServiceClient.uploadRefundReport(excelBytes);
 * </pre>
 * <p>
 *     注意事项:
 * <pre>
 *   行号计算：
 *     起始行始终为1（Excel第2行，跳过标题行）
 *     行数计算应包含所有子行
 *   ID唯一性：
 *     ID应为可比较的对象（实现equals和hashCode）
 *     null值会被忽略，不会参与合并
 *   列索引：
 *     列索引从0开始
 *     确保索引不超过表头列数
 *   性能考虑：
 *     适合中小数据量导出（<10万行）
 *     大数据量导出建议分页处理
 *
 * @author SongRenShuo
 * @date 2025/08/05
 */
public class EnhancedExcelExporter {

    /**
     * 导出到文件路径
     * <p>
     *  调用示例：
     * <pre>
     *         // 1. 转换为导出DTO
     *         List<RefundPackageExportDTO> exportData = convertToExportDTO02(dataList);
     *
     *         // 2. 创建导出配置
     *         EnhancedExcelExporter.MergeStrategy<RefundPackageExportDTO> idBasedMergeStrategy = EnhancedExcelExporter.createIdBasedMergeStrategy(
     *                 RefundPackageExportDTO::getPackageId, // ID提取器
     *                 dto -> 1,                             // 每个DTO占一行
     *                 0, 1, 2, 3, 4, 5, 6, 7, 8, 9         // 合并列索引
     *         );
     *
     *         EnhancedExcelExporter.ExportConfig<RefundPackageExportDTO> config =
     *                 new EnhancedExcelExporter.ExportConfig<>(RefundPackageExportDTO.class)
     *                         .withMergeStrategy(idBasedMergeStrategy);
     *
     *         // 3. 执行导出到文件
     *         String filePath = "D:\\Desktop\\退货管理.xlsx";
     *         EnhancedExcelExporter.export(filePath, "退货管理", exportData, config);
     *
     * @param filePath 文件路径
     * @param sheetName 工作表名称
     * @param data 数据列表
     * @param config 导出配置
     * @param <T> 数据类型
     */
    @SuppressWarnings("unused")
    public static <T> void export(String filePath,
                                  String sheetName,
                                  List<T> data,
                                  ExportConfig<T> config) {
        // 确保目录存在
        File file = new File(filePath);
        ifDirectoryDoesNotExistItIsAutomaticallyCreated(file.getParentFile());

        // 检查是否为追加模式且文件已存在
        boolean fileExists = file.exists() && file.length() > 0;
        boolean appendMode = config.isAppend() && fileExists;

        // 创建ExcelWriter
        try (ExcelWriter excelWriter = EasyExcel.write(file, config.getDataClass())
                .withTemplate(appendMode ? file : null)
                .build()) {
            // 执行导出
            doExport(excelWriter, sheetName, data, config, appendMode);
        }
    }

    /**
     * 导出到输出流
     * <p>
     *  调用示例：
     *     <pre>
     *      &#064;GetMapping("/export/refunds")
     *     public void exportRefunds(HttpServletResponse response) {
     *         try {
     *             // 1. 模拟查询数据
     *             List<RefundPackage> dataList = getRefundPackages();
     *
     *             // 2. 转换为导出DTO
     *             List<RefundPackageExportDTO> exportData = convertToExportDTO(dataList);
     *
     *             // 3. 创建合并策略
     *             EnhancedExcelExporter.MergeStrategy<RefundPackageExportDTO> mergeStrategy =
     *                 EnhancedExcelExporter.createIdBasedMergeStrategy(
     *                     RefundPackageExportDTO::getPackageId, // ID提取器
     *                     dto -> 1,                             // 每个DTO占一行
     *                     0, 1, 2, 3, 4, 5, 6, 7, 8, 9         // 合并列索引
     *                 );
     *
     *             // 4. 创建导出配置
     *             EnhancedExcelExporter.ExportConfig<RefundPackageExportDTO> config =
     *                 new EnhancedExcelExporter.ExportConfig<>(RefundPackageExportDTO.class)
     *                     .withMergeStrategy(mergeStrategy);
     *
     *             // 5. 设置响应头
     *             response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
     *             response.setHeader("Content-Disposition", "attachment; filename=退货管理.xlsx");
     *
     *             // 6. 获取输出流并执行导出
     *             try (OutputStream out = response.getOutputStream()) {
     *                 EnhancedExcelExporter.export(out, "退货管理",exportData,config);
     *                 out.flush();
     *             }
     *         } catch (Exception e) {
     *             throw new RuntimeException("导出失败", e);
     *         }
     *     }
     *
     * @param outputStream 输出流
     * @param sheetName 工作表名称
     * @param data 数据列表
     * @param config 导出配置
     * @param <T> 数据类型
     */
    @SuppressWarnings("unused")
    public static <T> void export(OutputStream outputStream,
                                  String sheetName,
                                  List<T> data,
                                  ExportConfig<T> config) {
        // 确保目录存在
        File parentDir = new File(outputStream.toString()).getParentFile();
        ifDirectoryDoesNotExistItIsAutomaticallyCreated(parentDir);

        // 创建ExcelWriter（流导出不支持追加模式）
        try (ExcelWriter excelWriter = EasyExcel.write(outputStream, config.getDataClass()).build()) {
            // 执行导出（流导出总是覆盖模式）
            doExport(excelWriter, sheetName, data, config, false);
        }
    }

    /**
     * 如果目录不存在，则会自动创建它
     *
     * @param parentDir 父目录
     */
    private static void ifDirectoryDoesNotExistItIsAutomaticallyCreated(File parentDir) {
        if (parentDir != null && !parentDir.exists()) {
            boolean whetherCatalogCreationWasSuccessful = parentDir.mkdirs();
            if (!whetherCatalogCreationWasSuccessful) {
                throw new RuntimeException("创建目录失败：" + parentDir.getAbsolutePath());
            }
        }
    }

    /**
     * 执行导出核心逻辑
     *
     * @param appendMode 是否为追加模式
     */
    private static <T> void doExport(ExcelWriter excelWriter,
                                     String sheetName,
                                     List<T> data,
                                     ExportConfig<T> config,
                                     boolean appendMode) {
        // 配置单元格样式（垂直居中）
        WriteCellStyle contentStyle = new WriteCellStyle();
        contentStyle.setVerticalAlignment(VerticalAlignment.CENTER);

        // 创建合并处理器（追加模式下不应用合并策略）
        List<CellRangeAddress> mergeRegions = (config.getMergeStrategy() != null && !appendMode) ?
                config.getMergeStrategy().calculateMergeRegions(data) :
                Collections.emptyList();
        MergeHandler mergeHandler = new MergeHandler(mergeRegions);

        // 创建WriteSheet
        WriteSheet writeSheet = EasyExcel.writerSheet(sheetName)
                // 使用类定义生成表头（追加模式下不写表头）
                .head(appendMode ? null : config.getDataClass())
                .registerWriteHandler(mergeHandler)
                .registerWriteHandler(new HorizontalCellStyleStrategy(null, contentStyle))
                .build();

        // 写入数据
        excelWriter.write(data, writeSheet);
    }


    /**
     * 导出配置类
     */
    @Getter
    public static class ExportConfig<T> {
        /**
         * 数据类
         */
        private final Class<T> dataClass;
        /**
         * 合并策略
         */
        private MergeStrategy<T> mergeStrategy;
        /**
         * 是否为追加模式(默认覆盖模式)
         */
        private boolean append = false;

        public ExportConfig(Class<T> dataClass) {
            this.dataClass = dataClass;
        }

        /**
         * 使用合并策略
         *
         * @param mergeStrategy 合并策略
         * @return {@link ExportConfig }<{@link T }>
         */
        public ExportConfig<T> withMergeStrategy(MergeStrategy<T> mergeStrategy) {
            this.mergeStrategy = mergeStrategy;
            return this;
        }

        /**
         * 设置追加模式
         *
         * @param append 是否为追加模式
         * @return {@link ExportConfig }<{@link T }>
         */
        public ExportConfig<T> withAppend(boolean append) {
            this.append = append;
            return this;
        }
    }

    /**
     * 合并策略接口
     */
    public interface MergeStrategy<T> {
        /**
         * 计算合并区域
         *
         * @param data 数据
         * @return {@link List }<{@link CellRangeAddress }>
         */
        List<CellRangeAddress> calculateMergeRegions(List<T> data);
    }

    /**
     * 合并处理器
     */
    private static class MergeHandler extends AbstractMergeStrategy {
        private final List<CellRangeAddress> mergeRegions;
        private final Set<String> mergedRegions = new HashSet<>();

        public MergeHandler(List<CellRangeAddress> mergeRegions) {
            this.mergeRegions = mergeRegions;
        }

        /**
         * 合并
         *
         * @param sheet 表
         * @param cell 单元格
         * @param head 头
         * @param relativeRowIndex 相对行索引
         *
         */
        @Override
        protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
            if (relativeRowIndex != 0 || mergeRegions == null) {
                return;
            }

            for (CellRangeAddress region : mergeRegions) {
                String regionKey = region.formatAsString();
                if (mergedRegions.contains(regionKey)) {
                    continue;
                }
                try {
                    sheet.addMergedRegion(region);
                    mergedRegions.add(regionKey);
                } catch (IllegalStateException e) {
                    // 忽略重复合并异常
                    System.err.println("跳过重复合并区域: " + regionKey);
                }

            }

        }
    }

    // ====================== 合并策略工具方法 ======================

    /**
     * 创建基于ID的合并策略
     *
     * @param idExtractor id 提取器
     * @param rowCounter 行计数器
     * @param columnsToMerge 要合并列
     * @return {@link MergeStrategy }<{@link T }>
     */
    public static <T> MergeStrategy<T> createIdBasedMergeStrategy(
            Function<T, Object> idExtractor,
            Function<T, Integer> rowCounter,
            int... columnsToMerge) {

        return data -> {
            List<CellRangeAddress> mergeRegions = new ArrayList<>();
            Map<Object, Integer> idStartRowMap = createHashMapWithExpectedSize(data.size());
            Map<Object, Integer> idRowCountMap = createHashMapWithExpectedSize(data.size());
            // 从第2行开始(第1行为标题)
            int currentRow = 1;

            for (T item : data) {
                int itemCount = rowCounter.apply(item);
                Object id = idExtractor.apply(item);

                if (id != null) {
                    if (idStartRowMap.containsKey(id)) {
                        // 如果ID已存在，增加行数计数
                        idRowCountMap.put(id, idRowCountMap.get(id) + itemCount);
                    } else {
                        // 如果是新ID，记录起始行和初始行数
                        idStartRowMap.put(id, currentRow);
                        idRowCountMap.put(id, itemCount);
                    }
                }
                currentRow += itemCount;
            }
            
            // 根据收集的信息创建合并区域
            for (Map.Entry<Object, Integer> entry : idStartRowMap.entrySet()) {
                Object id = entry.getKey();
                int startRow = entry.getValue();
                int rowCount = idRowCountMap.get(id);
                
                // 只有当行数大于1时才需要合并
                if (rowCount > 1) {
                    int endRow = startRow + rowCount - 1;
                    for (int col : columnsToMerge) {
                        mergeRegions.add(new CellRangeAddress(startRow, endRow, col, col));
                    }
                }
            }
            
            return mergeRegions;
        };
    }


    /**
     * 函数式接口（简化版）
     */
    @FunctionalInterface
    public interface Function<T, R> {
        /**
         * 应用
         *
         * @param t t
         * @return {@link R }
         */
        R apply(T t);
    }


    /**
     * 创建具有预期大小哈希映射
     *
     * @param expectedSize 预期尺寸
     * @return {@link Map }<{@link K }, {@link V }>
     */
    private static <K, V> Map<K, V> createHashMapWithExpectedSize(Integer expectedSize) {
        expectedSize = expectedSize == null ? 16 : expectedSize;
        return new HashMap<>((int) (expectedSize / 0.75f) + 1);
    }

    /**
     * 退款包导出 DTO
     */
    @Data
    public static class RefundPackageExportDTO {

        // ====================== 包裹信息 ======================
        @ExcelProperty("包裹ID")
        private Integer packageId;

        @ExcelProperty("包裹编码")
        private String packageCode;

        @ExcelProperty("订单编码")
        private String orderCode;

        @ExcelProperty("退货物流单号")
        private String returnTrackingNumber;

        @ExcelProperty(value = "快递类型（1.顺丰，2.圆通，3.申通，4.中通，5.EMS，6.韵达，7.天天，8.极兔，9京东）")
        private Integer courierType;

        @ExcelProperty(value = "包裹损坏程度（1.全坏、2.完整、3.半损）")
        private Integer packageDamageLevel;

        @ExcelProperty(value = "退货类型（1未知、2未揽收拦截、3途中拦截、4拒收、5签收后退回）")
        private Integer returnType;

        @ExcelProperty("备注")
        private String remark;

        @ExcelProperty("录入人")
        private String creator;

        @ExcelProperty("录入时间")
        @ColumnWidth(16)
        private Date createTime;

        // ====================== 货品信息 ======================
        @ExcelProperty("货品编码")
        private String itemCode;

        @ExcelProperty("货品名称")
        private String itemName;

        @ExcelProperty("货品数量")
        private Integer itemQuantity;

        @ExcelProperty("货品损坏程度（1.全坏、2.完整、3.半损）")
        private Integer itemDamageLevel;

        @ExcelProperty("货品备注")
        private String itemRemark;

    }


    /**
     * 退货包裹实体类
     */
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class RefundPackage {
        /**
         *  包裹ID
         */
        private Integer id;
        /**
         *  包裹编码
         */
        private String packageCode;
        /**
         *  订单编码
         */
        private String orderCode;
        /**
         *  退货物流单号
         */
        private String returnTrackingNumber;
        /**
         *  快递类型（1.顺丰，2.圆通，...）
         */
        private Integer courierType;
        /**
         *  包裹损坏程度（1.全坏、2.完整、3.半损）
         */
        private Integer damageLevel;
        /**
         *  退货类型（1未知、2未揽收拦截、3途中拦截、4拒收、5签收后退回）
         */
        private Integer returnType;
        /**
         *  备注
         */
        private String remark;
        /**
         *  录入人
         */
        private String creator;
        /**
         *  录入时间
         */
        private Date createTime;
        /**
         *  货品列表
         */
        private List<RefundItem> items;


        /**
         * 退货货品实体类
         */
        @Data
        @AllArgsConstructor
        @NoArgsConstructor
        public static class RefundItem {
            /**
             *  货品编码
             */
            private String itemCode;
            /**
             *  货品名称
             */
            private String itemName;
            /**
             *  货品数量
             */
            private Integer quantity;
            /**
             *  货品损坏程度（1.全坏、2.完整、3.半损）
             */
            private Integer damageLevel;
            /**
             *  货品备注
             */
            private String remark;
        }
    }
}
